Skip to content

Add video_contact_sheet: Generate contact sheets from videos using keyframes #426

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

libran11
Copy link

@libran11 libran11 commented May 13, 2025

🚀 Add video_contact_sheet: Generate contact sheets from videos using keyframes

Summary

This PR introduces a new advanced utility script video_contact_sheet which generates visual contact sheets from video files using scene-change keyframes.

Description

This tool extracts keyframes based on histogram difference, composes them into a grid with metadata (duration, resolution, codec), and exports them as JPEG images. It is particularly useful for dataset QA, content review, or automated preview generation.

The script supports both single-file and batch directory input, and includes multi-threaded processing for high efficiency. Full CLI interface and pytest-based test cases are included.

The changes are as follows:

- Added video_contact_sheet/ submodule with the following files:
- cli.py: CLI entry using click
- core.py: Core logic for keyframe extraction and contact sheet composition
- utils.py: FFmpeg metadata fetching and thread pool management
- tool_test/test_contact_sheet.py: Unit test using a generated sample video
- requirements.txt: Light dependencies list
- README.md: Project documentation
- Provided random sample video and automated testing
- Compatible with Pillow ≥10 (textbbox used)

Checks

in the repository

•	Made no changes that degrades the functioning of the repository
•	Gave each commit a better title (unlike updated README.md)

in the PR

•	Followed the format of the pull_request_template
•	Made the Pull Request in a small level (for the creator’s wellfare)
•	Tested the changes you made

Related issue links

@DhanushNehru DhanushNehru requested a review from Copilot July 21, 2025 16:41
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces a new video processing utility called video_contact_sheet that generates visual contact sheets (filmstrip grids) from video files using scene-change keyframe detection. The tool is designed for video dataset QA, content review, and automated preview generation.

  • Core functionality for keyframe extraction using histogram difference and contact sheet composition
  • CLI interface with multi-threading support for batch processing
  • Utility functions for FFmpeg metadata extraction and parallel processing

Reviewed Changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
video_contact_sheet/utils.py FFmpeg metadata extraction and thread pool utilities
video_contact_sheet/requirements.txt Project dependencies specification
video_contact_sheet/core.py Core keyframe extraction and contact sheet generation logic
video_contact_sheet/cli.py Command-line interface with click framework
video_contact_sheet/init.py Package initialization and version definition
video_contact_sheet/README.md Documentation and usage examples

Comment on lines +25 to +28
proc = subprocess.run(
FFPROBE_CMD + [str(path)], capture_output=True, text=True, check=True
)
return json.loads(proc.stdout)
Copy link
Preview

Copilot AI Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The subprocess.run call lacks proper error handling. When check=True, subprocess.CalledProcessError will be raised but not handled, making it difficult for users to understand what went wrong with ffprobe execution.

Suggested change
proc = subprocess.run(
FFPROBE_CMD + [str(path)], capture_output=True, text=True, check=True
)
return json.loads(proc.stdout)
try:
proc = subprocess.run(
FFPROBE_CMD + [str(path)], capture_output=True, text=True, check=True
)
return json.loads(proc.stdout)
except subprocess.CalledProcessError as e:
raise RuntimeError(
f"ffprobe command failed for path {path}. Error: {e.stderr.strip()}"
) from e

Copilot uses AI. Check for mistakes.

@@ -0,0 +1,115 @@
"""
Core logic: Keyframe extraction, contact table splicing.
Copy link
Preview

Copilot AI Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The term 'contact table splicing' should be 'contact sheet composition' to match the actual functionality and be consistent with terminology used elsewhere in the codebase.

Suggested change
Core logic: Keyframe extraction, contact table splicing.
Core logic: Keyframe extraction, contact sheet composition.

Copilot uses AI. Check for mistakes.


def _collect_meta(video_path: Path) -> Dict:
info = ffprobe_metadata(video_path)
v_stream = next(s for s in info["streams"] if s["codec_type"] == "video")
Copy link
Preview

Copilot AI Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The next() function will raise StopIteration if no video stream is found. This should use next() with a default value or proper exception handling to avoid crashes when processing files without video streams.

Suggested change
v_stream = next(s for s in info["streams"] if s["codec_type"] == "video")
v_stream = next((s for s in info["streams"] if s["codec_type"] == "video"), None)
if v_stream is None:
raise ValueError(f"No video stream found in file: {video_path}")

Copilot uses AI. Check for mistakes.

Comment on lines +15 to +18
Scene-change detection for “interesting” keyframes
Multithreaded extraction using OpenCV + ffmpeg
Footer shows duration / resolution / codec
Pure-Python, works on Windows/Linux/macOS
Copy link
Preview

Copilot AI Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The markdown formatting is missing proper bullet points. Lines 15-18 should be formatted as a proper markdown list with '- ' prefix for each feature.

Suggested change
Scene-change detection for “interesting” keyframes
Multithreaded extraction using OpenCV + ffmpeg
Footer shows duration / resolution / codec
Pure-Python, works on Windows/Linux/macOS
- Scene-change detection for “interesting” keyframes
- Multithreaded extraction using OpenCV + ffmpeg
- Footer shows duration / resolution / codec
- Pure-Python, works on Windows/Linux/macOS

Copilot uses AI. Check for mistakes.

Comment on lines +52 to +61
FONT = ImageFont.load_default()


def make_contact_sheet(
frames: List[np.ndarray],
metadata: Dict,
cols: int = 5,
margin: int = 8,
) -> Image.Image:

Copy link
Preview

Copilot AI Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The global font variable should be initialized inside the function or made configurable. Global state can cause issues in multi-threaded environments and makes the code less flexible.

Suggested change
FONT = ImageFont.load_default()
def make_contact_sheet(
frames: List[np.ndarray],
metadata: Dict,
cols: int = 5,
margin: int = 8,
) -> Image.Image:
def make_contact_sheet(
frames: List[np.ndarray],
metadata: Dict,
cols: int = 5,
margin: int = 8,
font: ImageFont.ImageFont = None,
) -> Image.Image:
if font is None:
font = ImageFont.load_default()

Copilot uses AI. Check for mistakes.

Comment on lines +30 to +35
vids = []
for p in inputs:
if p.is_dir():
vids.extend(list(p.rglob("*.mp4")))
else:
vids.append(p)
Copy link
Preview

Copilot AI Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The video discovery is hardcoded to only find .mp4 files. Consider supporting multiple video formats or making the file extensions configurable to handle common video formats like .avi, .mov, .mkv, etc.

Suggested change
vids = []
for p in inputs:
if p.is_dir():
vids.extend(list(p.rglob("*.mp4")))
else:
vids.append(p)
# Define supported video extensions
supported_extensions = [".mp4", ".avi", ".mov", ".mkv"]
vids = []
for p in inputs:
if p.is_dir():
for ext in supported_extensions:
vids.extend(list(p.rglob(f"*{ext}")))
else:
if p.suffix in supported_extensions:
vids.append(p)

Copilot uses AI. Check for mistakes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants